﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace FileMergeAndSplitTool
{
    /// <summary>
    ///  A class, which builds a regular expression in order to parse strings given a set of tokens and a grammar string.
    /// </summary>
    /// <typeparam name="SemanticsEnumType">
    ///  Each token is given a 'semantic value', which is used (by the calling program, not this class) to 
    ///  decide what to do with each value that is returned by this parser.
    /// </typeparam>
    class RegexBuilder<SemanticsEnumType>
    {

        #region PrivateInstanceVariables

        private Dictionary<string, string> tokens = new Dictionary<string, string>();
        private Dictionary<string, string> tokenDescriptors = new Dictionary<string, string>();
        private Dictionary<string, SemanticsEnumType> tokenSemantics = new Dictionary<string, SemanticsEnumType>();
        private List<SemanticsEnumType> tokenOrder;
        private Dictionary<SemanticsEnumType, HashSet<string>> inverseTokenSemantics = new Dictionary<SemanticsEnumType, HashSet<string>>();
        private string patternString;
        private Regex regex = new Regex(".*");

        #endregion // PrivateInstanceVariables

        #region Properties

        /// <summary>
        ///     A list of the string tokens that the parser will recognise.
        /// </summary>
        public List<string> Tokens
        {
            get { return new List<string>(tokens.Keys); }
        }

        /// <summary>
        ///     A dictionary of string tokens to user friendly descriptions for each token in this parser.
        /// </summary>
        public Dictionary<string, string> TokenDescriptors 
        {
            get { return new Dictionary<string,string>(tokenDescriptors); }
        }

        /// <summary>
        ///     A dictionary of the semantic value associated with each token
        /// </summary>
        public Dictionary<string, SemanticsEnumType> TokenSemantics
        {
            get { return new Dictionary<string,SemanticsEnumType>(tokenSemantics); }
        }  

        /// <summary>
        ///     The pattern string (grammar) this parser will use in order to parse input.
        /// </summary>
        public string PatternString
        {
            get { return patternString; }

            set
            {
                patternString = value;
                Dictionary<SemanticsEnumType, int> partIdToPositionMap = new Dictionary<SemanticsEnumType, int>();
                foreach (string token in TokenSemantics.Keys)
                {
                    //find where this token occurs and store it
                    int pos = patternString.IndexOf(token);
                    if (pos < 0)
                        continue;
                    partIdToPositionMap.Add(TokenSemantics[token], pos);
                }

                tokenOrder = new List<SemanticsEnumType>(partIdToPositionMap.Keys);
                tokenOrder.Sort(delegate(SemanticsEnumType a, SemanticsEnumType b) { return partIdToPositionMap[a].CompareTo(partIdToPositionMap[b]); });

                regex = GetRegularExpressionForString(patternString);
            }
        }

        /// <summary>
        ///     The regular expression that will match strings of the grammar specified in PatternString
        /// </summary>
        public Regex Regex
        {
            get { return regex; }
        }

        #endregion // Properties

        #region Constructors

        public RegexBuilder()
        {

        }

        #endregion // Constructors


        #region PublicMethods
        /// <summary>
        ///     Adds a token to the list of tokens to be recognised by the parser.
        /// </summary>
        /// <param name="token">The string token to be recognised
        ///     <example>&quot;%number%&quot;</example>
        /// </param>
        /// <param name="regex">The regular expression that describes the strings this token should match.
        ///     <example>[0-9]+</example>
        /// </param>
        /// <param name="userFriendlyDescription">A user-friendly description, that will be showed on help pages, of what this token does.</param>
        /// <param name="semanticEnumValue">Some semantic value to be associated with this token that allows you to know what to do with the output, when it is parsed.</param>
        public void AddToken(string token, string regex, string userFriendlyDescription, SemanticsEnumType semanticEnumValue)
        {
            tokens.Add(token, regex);
            tokenDescriptors.Add(token, userFriendlyDescription);
            tokenSemantics.Add(token, semanticEnumValue);
            AddInverseTokenSemantics(token, semanticEnumValue);
        }

        /// <summary>
        ///     Returns a list of tokens which have a specific semantic value associated with them
        /// </summary>
        /// <param name="s">The semantic value for which to get tokens</param>
        /// <returns>A list of the token strings that are associated with the semantic value, s.</returns>
        public List<string> GetTokensWithSemantics(SemanticsEnumType s)
        {
            if (!inverseTokenSemantics.ContainsKey(s) || inverseTokenSemantics[s] == null)
            {
                return new List<string>();
            }
            else
            {
                return new List<string>(inverseTokenSemantics[s]);
            }       
        }

        /// <summary>
        ///     Parses the input string, returning a dictionary map of semantic values to the matched text from the input string.
        /// </summary>
        /// <param name="inputString">The string to be parsed</param>
        /// <returns>A dictionary map of semantic values to the matched text from the input string (a list, as it is possible for tokens to be repeated in the pattern string)</returns>
        public Dictionary<SemanticsEnumType, List<string>> Parse(string inputString)
        {
            Dictionary<SemanticsEnumType, List<string>> output = new Dictionary<SemanticsEnumType, List<string>>();
            MatchCollection mc = regex.Matches(inputString);

            foreach(SemanticsEnumType semantic in inverseTokenSemantics.Keys)
            {
                if (!output.ContainsKey(semantic) || output[semantic] == null)
                {
                    output.Add(semantic, new List<string>());
                }

                if (mc.Count > 0)
                {
                //This is a match
                    string capturedValue = mc[0].Groups[1 + tokenOrder.IndexOf(semantic)].Value;
                    output[semantic].Add(capturedValue);
                }
            }

            return output;
        }

        /// <summary>
        ///     Replaces occurrences in the inputString of string tokens that are associated with semanticType with some replacement string (replaceWith)
        /// </summary>
        /// <param name="inputString">The string in which to have string tokens replaced</param>
        /// <param name="semanticType">The semantic value of the token to be replaced</param>
        /// <param name="replaceWith">The string with which to replace tokens that mach the semanticType in the input string</param>
        /// <returns></returns>
        public string ReplaceSemanticToken(string inputString, SemanticsEnumType semanticType, string replaceWith)
        {
            string outputString = inputString;
            foreach(string token in this.GetTokensWithSemantics(semanticType))
            {
                outputString = outputString.Replace(token, replaceWith);
            }

            return outputString;
        }

        #endregion


        #region PrivateMethods

        /// <summary>
        ///     Adds to a dictionary of semantic values -> tokens, in order to allow fast inverse lookups of string tokens based on semantic value.
        /// </summary>
        /// <param name="token">The string token</param>
        /// <param name="semanticEnumValue">The semantic value</param>
        private void AddInverseTokenSemantics(string token, SemanticsEnumType semanticEnumValue)
        {
            if (!inverseTokenSemantics.ContainsKey(semanticEnumValue) || inverseTokenSemantics[semanticEnumValue] == null)
            {
                inverseTokenSemantics.Add(semanticEnumValue, new HashSet<string>());
            }

            inverseTokenSemantics[semanticEnumValue].Add(token);
        }

        /// <summary>
        ///     Returns a regular expression object that will match strings of the grammar specified in patternString
        /// </summary>
        /// <param name="patternString"></param>
        /// <returns></returns>
        private Regex GetRegularExpressionForString(string patternString)
        {
            string regexString = EscapeCharacters(patternString, new string[] { "\\", ".", "*", "[", "]", "(", ")", "+", "{", "}", "?", "/" });

            foreach (string token in tokens.Keys)
            {
                regexString = regexString.Replace(token, tokens[token]);
            }
            return new Regex("^" + regexString + "$");
        }

        /// <summary>
        ///     Puts slashes in front of characters specified in the character list.
        /// </summary>
        /// <param name="input"></param>
        /// <param name="characters"></param>
        /// <returns></returns>
        private string EscapeCharacters(string input, string[] characters)
        {
            string output = input;
            foreach (string ch in characters)
            {
                output = output.Replace(ch, "\\" + ch);
            }

            return output;
        }

        #endregion
    }
}
